﻿/*
 

The MIT License

Copyright (c) 2008 Pablo Bandin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

 */
package  {
	import flash.errors.IllegalOperationError;
	import flash.events.ErrorEvent;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.events.Event;
	import flash.net.navigateToURL;
	import flash.net.URLRequest;
	import flash.external.ExternalInterface;
	
	/**
	* SWFTRACKER v.1.0 BETA 1
	* @author pablo bandin
	*/
	public class SWFTracker implements IEventDispatcher{
		
		/*
		 *  Embeded info for decompiled SWF
		 */
		public static const NAME     :String = 'SWFTracker';
		public static const VERSION  :String = 'Public Beta 1.0';
		public static const DATE     :String = 'November 8th, 2008';
		public static const AUTHOR   :String = 'Pablo Bandin';
		
		
		/**
		 * Event Type dispatched when an event is dispatched by the object.
		 */
		public static const EVENT_DISPATCHED:String = 'swftracker_edispatched';
		
		/**
		 * Default event type, when this object has no events.
		 */
		public static var DEFAULT_COMMAND:String 	= 'default';
		
		/**
		 * Hash command delimiter, you can change this if you want
		 */
		public static var HASH_COM_DELIMITER:String = '/';
		
		/**
		 * internal flag to know if javascript is available. 
		 */
		private static var jsEnabled:Boolean	= true;
		
		private var _dispatcher:EventDispatcher;
		
		/**
		 * Alias container, key->function
		 */
		private var functionAlias:Object;
		
		/**
		 * listeners created by parsing of Strings. Needed to garbage collection.
		 */
		private var anonymousListeners:Object;
		
		/**
		 * EventDispatcher type. So we dont create a new instance every time.
		 */
		private var evDispatched:Event;
		
		/**
		 * the current command of this object
		 */
		private var _currentCommand:String;
		
		/**
		 * child has after taking the command for this object.
		 */
		private var _childHash:String;
		
		/**
		 * to update url, 1 instance needed. 
		 */
		private var req:URLRequest;
		
		/**
		 * Parent node pointer
		 */
		private var _parent:SWFTracker;
		
		/**
		 * what command say to this object that must run?
		 */
		private var _parentCommandFilter:RegExp;
		
		/**
		 * if false, this object will not parse and listen any event.
		 */
		public var enabled:Boolean;
		
		/**
		 * SWFTracker constructor
		 * @param	parentTracker Parent node.
		 */
		public function SWFTracker(parentTracker:SWFTracker = null) {
			_dispatcher 			= new EventDispatcher(this);
			functionAlias 			= new Object();
			anonymousListeners 		= new Object();
			evDispatched 			= new Event(EVENT_DISPATCHED);
			_currentCommand			= DEFAULT_COMMAND;
			_childHash				= '';
			req						= new URLRequest();
			parent					= parentTracker;
			enabled					= true;
			_parentCommandFilter		= /.*/g;
		}
		
		/**
		 * just a string cleaner
		 * @private
		 */
		public static function cleanString(str:String):String {
			return str.replace(/\W*/g, '');
		}
		
		/**
		 * get an anonymous function. The ID will is important for garbage collection.
		 */
		private static function getAnonymousFunction(id:String, f:Function, parametros:Array = null):Function {
			return function(behavior:uint = 0):String {
				switch(behavior) {
					case 1:
						f = null;
						parametros = null;
						break;
					case 2:
						return id;
					default:
						f.apply(null, parametros);
				}
				return '';
			}
		}
		
		/**
		 * Current command of the object
		 */
		public function get currentCommand():String {
			return _currentCommand;
		}
		
		public function get parent():SWFTracker { return _parent;  }
		
		/**
		 * Set a parent for this object. This will create a listener for the EVENT_DISPATCHED event on the parent.
		 */
		public function set parent(newparent:SWFTracker):void {
			if (_parent != null)
				_parent._dispatcher.removeEventListener(EVENT_DISPATCHED, onParentCommandDispatched);
				
				
			_parent = newparent;
			if (_parent != null)
				_parent._dispatcher.addEventListener(EVENT_DISPATCHED, onParentCommandDispatched, false, 0, true);
				
		}
		
		/**
		 * command filter. If the parent command (event) match the filter, then the run() function is called.
		 */
		public function set parentCommandFilter(newFilter:RegExp):void {
			_parentCommandFilter = newFilter;
		}
		public function get parentCommandFilter():RegExp {
			return _parentCommandFilter;
		}
		
		
		/* INTERFACE flash.events.IEventDispatcher */
		
		/**
		 * Same as EventDispatcher functionality but, useCapture is ignored to false.
		 * 
		 * @inheritDoc
		 */
		public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void{		
			
			if (type == EVENT_DISPATCHED)
				throw new IllegalOperationError("Type Event: " + type + " is reserved for internal use only.");
			_dispatcher.addEventListener(cleanString(type), listener, false, priority, useWeakReference);
		}
		
		/**
		 * this fucntion is not used.
		 */
		public function dispatchEvent(event:Event):Boolean{
			throw new IllegalOperationError("This function is not implemented, use dispatchTrackerEvent instead.");
		}
		
		
		/**
		 * @inheritDoc
		 */
		public function hasEventListener(type:String):Boolean{
			return _dispatcher.hasEventListener(cleanString(type));
		}
		
		/**
		 * useCapture is set to False.
		 * 
		 * @inheritDoc
		 */
		public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void{
			_dispatcher.removeEventListener(cleanString(type), listener, false);
		}
		
		/**
		 * @inheritDoc
		 */
		public function willTrigger(type:String):Boolean{
			return _dispatcher.willTrigger(cleanString(type));
		}
		
		/*   ---   */
		
		/**
		 * Remove all functions created by addEventListenerByString()
		 * @param	type	event type of the listeners
		 * @see #addEventListenersFromString()
		 */
		public function removeAllAnonymousEventListeners(type:String):void {
			type = cleanString(type);
			if (!anonymousListeners.hasOwnProperty(type))
				return;
				
			for (var i:String in anonymousListeners[type]) {
				removeEventListener(type, anonymousListeners[type][i]);
				anonymousListeners[type][i](1);
				anonymousListeners[type][i] = null;
			}
			
			anonymousListeners[type] = [];
		}
		
		/**
		 * Removes one or more listeners. It makes the oposite of addEventListenersFromString()
		 * @param	type	Event type.
		 * @param	stringAlias	  alias functions
		 * @see #addEventListenersFromString()
		 */
		public function removeEventListenersFromString(type:String, stringAlias:String):void {
			type = cleanString(type);
			
			if (!anonymousListeners.hasOwnProperty(type))
				return;
			var arr:Array = stringAlias.match(/[^\)]+\)/g);
			var funcid:String;
			
			for(var i:String in arr){
				 funcid = (/(\w+)\([^\)]*\)/g).exec(arr[i])[0];
				 
				 for(var j:String in anonymousListeners[type]){
					 if (anonymousListeners[type][j](2) == cleanString(funcid)) {
						 
						 removeEventListener(type, anonymousListeners[type][j]);
						 anonymousListeners[type][j](1);
						 anonymousListeners[type].splice(j, 1);
					 }
				 }
				 //
			}
		}
		
		
		/**
		 * Add eventlisteners from a string, is an alias is found to match. A function is created and the parameters inside the () are bind to the function.
		 * @param	type	event type
		 * @param	stringAlias		alias string
		 * @see #removeEventListenersFromString()
		 * @see #removeAllAnonymousEventListeners()
		 */
		public function addEventListenersFromString(type:String, stringAlias:String):void {
			
			type = cleanString(type);
			
			if (type == EVENT_DISPATCHED)
				throw new IllegalOperationError("Type Event: " + type + " is reserved for internal use only.");
			
			var arr:Array = stringAlias.match(/[^\)]+\)/g);
			var needAnonymousListeners:Boolean = !anonymousListeners.hasOwnProperty(type);
			var sub:Array;
			var f:Function;
			
			for(var i:String in arr){
				 sub = (/(\w+)\(([^\)]*)\)/g).exec(arr[i]);
				 
				 if (functionAlias.hasOwnProperty(sub[1])) {
					 
					 f = getAnonymousFunction(cleanString(sub[0]), functionAlias[sub[1]], (sub.length > 2 && sub[2] != '')?sub[2].split(','):null);
					 
					 if (needAnonymousListeners){
						anonymousListeners[type] = new Array();
						needAnonymousListeners = false;
					 }
					 
						anonymousListeners[type].push(f);
					addEventListener(type, f, false, 0, true);
				 }
			}
		}
		
		
		/**
		 * Create a function alias so it can be used later in addEventListenersFromString()
		 * @param	aliasName			alias of the function.
		 * @param	functionCallback	the real function to refernce.
		 * @param	ifExistsReplace		Overrides the previous definition.
		 * @return	false if ifExistsReplace==true and the alias exists
		 */
		public function defineFunctionAlias(aliasName:String, functionCallback:Function, ifExistsReplace:Boolean = true):Boolean {
			aliasName = cleanString(aliasName);
			
			if(!functionAlias.hasOwnProperty(aliasName) || ifExistsReplace){
				functionAlias[aliasName] = functionCallback;
				return true;
			} else {
				return false;
			}
		}
		
		/**
		 * Delete a function alias
		 * @param	aliasName
		 */
		public function deleteFunctionAlias(aliasName:String):void {
			aliasName = cleanString(aliasName);
			functionAlias[aliasName] = null;
		}
		
		/**
		 * is like using dispatchEvent  but the event cant be declared in the capture state and it cant have bubble=true.
		 */
		public function dispatchTrackerEvent(EventType:String = null):void {
			dispatchCommandFromHash(cleanString(EventType) || DEFAULT_COMMAND, false);
		}
		
		/**
		 * For internal use. If the dispatch command comes from parsing the hash, dont update the hash every time. Wait for the last command.
		 * @param	commandName
		 * @param	mantenerChildHash
		 * @return
		 */
		private function dispatchCommandFromHash(commandName:String, mantenerChildHash:Boolean):Boolean {
				
			if (!hasEventListener(commandName) && commandName!=DEFAULT_COMMAND){
				commandName = DEFAULT_COMMAND;
				mantenerChildHash = false;
			}
			
				var prevCommand:String = _currentCommand;
				_currentCommand = commandName;
				
				if (!mantenerChildHash) {
					
					_childHash = '';
					
					if(commandName!=DEFAULT_COMMAND)
					updateURLHASH();
				}
				
				if(!mantenerChildHash || commandName!=prevCommand)
				_dispatcher.dispatchEvent(new Event(commandName));
			
			_dispatcher.dispatchEvent(evDispatched);
			return true;
		}
		
		
		/**
		 * Updates the url hash by passig an accumulated url to the parent until parent=null.
		 * @param	comandosAcumulados
		 */
		private function updateURLHASH(comandosAcumulados:String=''):void{
			comandosAcumulados = currentCommand + HASH_COM_DELIMITER + comandosAcumulados;
			if (parent != null) {
				parent.updateURLHASH(comandosAcumulados);
			}else {
				req.url = '#/' + comandosAcumulados;
				
				if(jsEnabled)
				navigateToURL(req, '_self');
			}
		}
		
		/**
		 * If parent runs a command this is called
		 */
		private function onParentCommandDispatched(e:Event):void {
			//
			if(_parentCommandFilter.test(SWFTracker(e.target).currentCommand))
			run();
		}
		
		/**
		 * Forces the object to read the parentChildHash or url hash.
		 */
		public function run():void {
			
			if (!enabled)
				return;
			
			var hash:String;
			
			if(parent==null){
				if (!ExternalInterface.available || (hash = ExternalInterface.call('getHash')) == null) {
					jsEnabled = false;
					hash = '';
					trace('SWFTracker WARNING:  function getHash(){return window.location.hash} not found!');
					
				}
				
			} else {
				hash = parent._childHash;
			}
			
			parseHash(hash.replace(/#\/?/g, ''));
		}
		
		/**
		 * Split the hash into commands and pass it to the childs by saving it in the _chidHash.
		 */
		private function parseHash(hash:String):void {
			
			var com:String 	= hash.replace(/\s/g, '');
			var indx:Number = com.indexOf(HASH_COM_DELIMITER);
			_childHash 		= indx > 0?com.substr(Math.max(com.indexOf(HASH_COM_DELIMITER),1)+1):'';
			com 			= com.substr(0, uint(indx));
			//
			dispatchCommandFromHash(com == ''?DEFAULT_COMMAND:com, _childHash!='');
		}
		
	}
	
}